{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "80099db0-306d-4341-bf4d-646580830c71",
   "metadata": {
    "jp-MarkdownHeadingCollapsed": true,
    "tags": []
   },
   "source": [
    "# Embedding基础"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "481c4d0c-918c-47c4-be83-c4aa97729e74",
   "metadata": {},
   "source": [
    "对于自然语言，因为它的输入是一段文本，在中文里就是一个一个字，或一个一个词，行业内把这个**字**或**词**叫**Token**。如果要使用模型，拿到一段文本的第一件事就是把它Token化，当然，可以按字、也可以按词，或按你想要的其他方式，比如每两个字一组（Bi-Gram）。举个例子：\n",
    "\n",
    "给定文本：我们相信AI可以让世界变得更美好。</br>\n",
    "按字Token化：我/们/相/信/A/I/可/以/让/世/界/变/得/更/美/好/。</br>\n",
    "按词Token化：我们/相信/AI/可以/让/世界/变得/更/美好/。</br>\n",
    "按Bi-Gram Token化：我们/们相/相信/信A/AI/I可/可以/以让/让世/世界/界变/变得/得更/更美/美好/好。</br>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b5a8f77a-68fa-4762-b287-fcaaac7cc7e4",
   "metadata": {},
   "source": [
    "- 有了Token以后，模型针对词典对每一个词转化为**整型数字**，如：\n",
    "\n",
    "我：1/们：2/我们：3/相：4/信：5/相信：6/...\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7faf53c-a9b1-4a3b-b1aa-0c87c76c406d",
   "metadata": {},
   "source": [
    "- 然后模型会把上面的每一个代表词的数字传入Embedding嵌入层中，嵌入层会将数据转化为D维的空间下能量。针对上述案例，最终Embedding后就会变成：\n",
    "\n",
    "> 我 0.xxx0, 0.yyy0, 0.zzz0, ... D个小数  \n",
    "们 0.xxx1, 0.yyy1, 0.zzz1, ... D个小数  \n",
    "相 0.xxx2, 0.yyy2, 0.zzz2, ... D个小数  \n",
    "信 0.xxx3, 0.yyy3, 0.zzz3, ... D个小数  \n",
    "……下面省略  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "93894927-9870-443a-9234-473050f2d894",
   "metadata": {},
   "source": [
    "简单的代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "2f27a3a1-12f6-4c59-92cd-d9f3b7c530c6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "16个词，256个词典embeding的维度为: (16, 256)\n",
      "对矩阵：\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "array([[0.77395605, 0.43887844, 0.85859792, ..., 0.24783956, 0.23666236,\n",
       "        0.74601428],\n",
       "       [0.81656876, 0.10527808, 0.06655886, ..., 0.11585672, 0.07205915,\n",
       "        0.84199321],\n",
       "       [0.05556792, 0.28061144, 0.33413004, ..., 0.00925978, 0.18832197,\n",
       "        0.03128351],\n",
       "       ...,\n",
       "       [0.50647331, 0.22303613, 0.94414565, ..., 0.79202324, 0.40169878,\n",
       "        0.72247782],\n",
       "       [0.9151384 , 0.80071297, 0.39044651, ..., 0.03994193, 0.79502741,\n",
       "        0.28297954],\n",
       "       [0.68255979, 0.64272531, 0.65262805, ..., 0.18645529, 0.21927175,\n",
       "        0.32320729]])"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import numpy as np\n",
    "rng = np.random.default_rng(42)\n",
    "# 词表大小N=16，维度D=256\n",
    "table = rng.uniform(size=(16, 256))\n",
    "print('16个词，256个词典embeding的维度为:', table.shape)\n",
    "print('对矩阵：')\n",
    "table"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "96f5bc2b-c737-4ee5-9b4f-c2ac7993f388",
   "metadata": {},
   "source": [
    "有了Embedding以后，模型就可以得到句子的数学表示</br>\n",
    "我们可以基于这个数学做**任何NLP任务**，其中最简单的就是相似度"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d469499-67de-4b8c-b20e-dfc457f8adcf",
   "metadata": {
    "jp-MarkdownHeadingCollapsed": true,
    "tags": []
   },
   "source": [
    "# ChatGPT API"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7a98b5c-8181-487a-b891-d8189ac1c9b1",
   "metadata": {},
   "source": [
    "## 获取ChatGPT的Embedding"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "f03bca33-b08f-45d4-b6db-a24ea32b829a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 获取ChatGPT的key，需提前将OPENAI_API_KEY配到系统变量中\n",
    "import os\n",
    "OPENAI_API_KEY = os.environ.get(\"OPENAI_API_KEY\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c91c37d0-5829-43a3-bf0e-f27c56443263",
   "metadata": {},
   "outputs": [],
   "source": [
    "import openai\n",
    "# 导入自己的API key\n",
    "openai.api_key = OPENAI_API_KEY"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8f555998-6cbf-4e1c-985d-36e94548fab6",
   "metadata": {},
   "outputs": [],
   "source": [
    "text = \"我喜欢你\" # 测试文案\n",
    "model = \"text-embedding-ada-002\" # embedding模型\n",
    "emb_req = openai.Embedding.create(input=[text], model=model) # 获取embedding"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "3d8f370f-1f48-404d-8f52-c03ccd19ceea",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1536, list)"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 打印输出\n",
    "emb = emb_req.data[0].embedding\n",
    "len(emb), type(emb)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75bc9db3-c6f2-4995-86b0-a5f9d9584bd4",
   "metadata": {},
   "source": [
    "&emsp;&emsp;与Embedding息息相关的一个概念是「相似度」，准确来说是「语义相似度」。在自然语言处理领域，我们一般使用**cosine相似度**作为语义相似度的度量，评估两个向量在语义空间上的分布情况。\n",
    "\n",
    "&emsp;&emsp;具体来说就是下面这个式子：\n",
    "\n",
    "$$\n",
    "\\text{cosine}(v,w) = \\frac {v·w}{|v||w|} = \\frac {\\displaystyle \\sum_{i=1}^N v_iw_i} {\\displaystyle \\sqrt{\\sum_{i=1}^N v_i^2} \\sqrt{\\sum_{i=1}^N w_i^2}}\n",
    "$$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6a5f6bf-e9f1-40f1-b797-cdead29e2d48",
   "metadata": {},
   "source": [
    "举个例子："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "3f15b1c0-3b76-4b17-872b-8910b11da621",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "模拟上述两句话的相似度为： 0.9925833339709301\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "a = [0.1, 0.2, 0.3]\n",
    "b = [0.2, 0.3, 0.4]\n",
    "cosine_ab = (0.1*0.2+0.2*0.3+0.3*0.4)/(np.sqrt(0.1**2+0.2**2+0.3**2) * np.sqrt(0.2**2+0.3**2+0.4**2))\n",
    "print('模拟上述两句话的相似度为：', cosine_ab)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58077fef-431b-403f-8c1c-787f127a600d",
   "metadata": {},
   "source": [
    "&emsp;&emsp;OpenAI官方提供了一个集成接口，使用起来更加简单（但其实你也可以自己写一个）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "7e7d6d89-ee64-4ffa-a2d1-23cc3262f380",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "embedding的类型为: <class 'list'>\n",
      "embedding的长度为: 12288\n"
     ]
    }
   ],
   "source": [
    "from openai.embeddings_utils import get_embedding, cosine_similarity\n",
    "\n",
    "# 注意它默认的模型是text-similarity-davinci-001，我们也可以换成text-embedding-ada-002\n",
    "text1 = \"我喜欢你\"\n",
    "text2 = \"我钟意你\"\n",
    "text3 = \"我不喜欢你\"\n",
    "emb1 = get_embedding(text1)\n",
    "emb2 = get_embedding(text2)\n",
    "emb3 = get_embedding(text3)\n",
    "\n",
    "print('embedding的类型为:', type(emb1))\n",
    "print('embedding的长度为:', len(emb1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "4c8504ba-e99b-424a-a298-8323e562e129",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 vs 2的相似度: 0.9246855139297095\n",
      "1 vs 3的相似度: 0.8578009661644184\n",
      "2 vs 3的相似度: 0.8205299527695253\n"
     ]
    }
   ],
   "source": [
    "print('1 vs 2的相似度:', cosine_similarity(emb1, emb2))\n",
    "print('1 vs 3的相似度:', cosine_similarity(emb1, emb3))\n",
    "print('2 vs 3的相似度:', cosine_similarity(emb2, emb3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd588a32-1523-4962-833d-3f789d79b573",
   "metadata": {},
   "source": [
    "&emsp;&emsp;text-embedding-ada-002模型在这个例子上表现不太令人满意。更多模型可以在这里查看：[New and improved embedding model](https://openai.com/blog/new-and-improved-embedding-model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c2036cfd-9a85-4cd7-b0b7-9d56a1b7534f",
   "metadata": {},
   "source": [
    "## ChatGPT风格的玩法"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c41f667e-7faa-4f8d-a2c1-96e8116414e7",
   "metadata": {},
   "source": [
    "按照ChatGPT的风格，显然不需要我们获取什么Embedding也是能满足相似度的计算的。所以我们可以通过提问直接获取答案："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "90a68284-6ed3-429d-b93d-293501bd0eaa",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'正相反。第一句表达了喜欢的情感，第二句则更加强烈地表达了喜欢和钟爱的感情，而第三句则完全相反，表达了不喜欢的情感。'"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "content = \"请告诉我下面三句话的相似程度：\\n1. {}。\\n2. {}。\\n3.{}。\\n\" # 定义提问模板\n",
    "\n",
    "# 提问ChatGPT\n",
    "response = openai.ChatCompletion.create(\n",
    "    model=\"gpt-3.5-turbo\", \n",
    "    messages=[{\"role\": \"user\", \"content\": content.format(text1, text2, text3)}]\n",
    ")\n",
    "# 获取答案\n",
    "response.get(\"choices\")[0].get(\"message\").get(\"content\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e2c3486-7bd7-4304-bd6b-f48a46b3d449",
   "metadata": {},
   "source": [
    "上述结果发现答案不怎么好，我们可以修改我们的prompt来得到我们想要的"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "90063efe-bfa3-4809-8f58-516bde2e0627",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'{\"ab\": 0.9, \"ac\": 0.2, \"bc\": 0.8}\\n\\n解释：\\n- a和b的相似度较高，因为两句话的意思都是表达喜欢的感情，只是词语不同而已；\\n- a和c的相似度较低，因为一句话表达喜欢，一句话表达不喜欢，意思完全相反；\\n- b和c的相似度较高，因为两句话都表达了某种感情，只是一个是喜欢，一个是不喜欢，但感情这一层面还是相通的。'"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "content = content.format(text1, text2, text3) + '第一句话用a表示，第二句话用b表示，第三句话用c表示，请以json格式输出两两相似度，类似下面这样：\\n{\"ab\": a和b的相似度}'\n",
    "\n",
    "response = openai.ChatCompletion.create(\n",
    "    model=\"gpt-3.5-turbo\", \n",
    "    messages=[{\"role\": \"user\", \"content\": content}]\n",
    ")\n",
    "response.get(\"choices\")[0].get(\"message\").get(\"content\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7f82ee75-1033-4308-9e7a-495490e6cb8d",
   "metadata": {},
   "source": [
    "# Embedding的应用"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "44d4a3a7-f148-4c0b-9fbb-bcd35e6e9be8",
   "metadata": {},
   "source": [
    "下面针对Embedding的应用列举几个基础应用"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34c20e89-b3de-46ea-946c-19c9f9a672b3",
   "metadata": {
    "jp-MarkdownHeadingCollapsed": true,
    "tags": []
   },
   "source": [
    "## QA"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "642ca298-9cde-4f8a-8f4e-d3f31f18b384",
   "metadata": {},
   "source": [
    "&emsp;&emsp;QA是问答的意思，Q表示Question，A表示Answer，QA是NLP非常基础和常用的任务。简单来说，就是当用户提出一个问题时，我们能从已有的问题库中找到一个最相似的，并把它的答案返回给用户。这里有两个关键点：\n",
    "\n",
    "1. 事先需要有一个QA库。\n",
    "2. 用户提问时，系统要能够在QA库中找到一个最相似的。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4bd1e054-d47c-4dae-902d-9e4e1061ced5",
   "metadata": {},
   "source": [
    "&emsp;&emsp;我们使用Kaggle提供的Quora数据集：[FAQ Kaggle dataset! | Data Science and Machine Learning](https://www.kaggle.com/general/183270)，先把它给读进来。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "e6185944-1c72-48a4-8b2d-ec278bde74f5",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "939b7a18-6c80-4b9d-bcf3-22c402aeb06d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Questions</th>\n",
       "      <th>Followers</th>\n",
       "      <th>Answered</th>\n",
       "      <th>Link</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>How do I start participating in Kaggle competi...</td>\n",
       "      <td>1200</td>\n",
       "      <td>1</td>\n",
       "      <td>/How-do-I-start-participating-in-Kaggle-compet...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Is Kaggle dead?</td>\n",
       "      <td>181</td>\n",
       "      <td>1</td>\n",
       "      <td>/Is-Kaggle-dead</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>How should a beginner get started on Kaggle?</td>\n",
       "      <td>388</td>\n",
       "      <td>1</td>\n",
       "      <td>/How-should-a-beginner-get-started-on-Kaggle</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>What are some alternatives to Kaggle?</td>\n",
       "      <td>201</td>\n",
       "      <td>1</td>\n",
       "      <td>/What-are-some-alternatives-to-Kaggle</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>What Kaggle competitions should a beginner sta...</td>\n",
       "      <td>273</td>\n",
       "      <td>1</td>\n",
       "      <td>/What-Kaggle-competitions-should-a-beginner-st...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1161</th>\n",
       "      <td>I took Andrew Ng's course on ML and I'm now pr...</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>/unanswered/I-took-Andrew-Ngs-course-on-ML-and...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1162</th>\n",
       "      <td>What are the various online programming contests?</td>\n",
       "      <td>237</td>\n",
       "      <td>1</td>\n",
       "      <td>/What-are-the-various-online-programming-contests</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1163</th>\n",
       "      <td>What are some good resources for learning R?</td>\n",
       "      <td>1300</td>\n",
       "      <td>1</td>\n",
       "      <td>/What-are-some-good-resources-for-learning-R-1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1164</th>\n",
       "      <td>What is the best way to start learning R? I wo...</td>\n",
       "      <td>96</td>\n",
       "      <td>1</td>\n",
       "      <td>/What-is-the-best-way-to-start-learning-R-I-wo...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1165</th>\n",
       "      <td>What are all the companies that Google has eve...</td>\n",
       "      <td>17</td>\n",
       "      <td>1</td>\n",
       "      <td>/What-are-all-the-companies-that-Google-has-ev...</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>1166 rows × 4 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                                              Questions  Followers  Answered  \\\n",
       "0     How do I start participating in Kaggle competi...       1200         1   \n",
       "1                                       Is Kaggle dead?        181         1   \n",
       "2          How should a beginner get started on Kaggle?        388         1   \n",
       "3                 What are some alternatives to Kaggle?        201         1   \n",
       "4     What Kaggle competitions should a beginner sta...        273         1   \n",
       "...                                                 ...        ...       ...   \n",
       "1161  I took Andrew Ng's course on ML and I'm now pr...          1         0   \n",
       "1162  What are the various online programming contests?        237         1   \n",
       "1163       What are some good resources for learning R?       1300         1   \n",
       "1164  What is the best way to start learning R? I wo...         96         1   \n",
       "1165  What are all the companies that Google has eve...         17         1   \n",
       "\n",
       "                                                   Link  \n",
       "0     /How-do-I-start-participating-in-Kaggle-compet...  \n",
       "1                                       /Is-Kaggle-dead  \n",
       "2          /How-should-a-beginner-get-started-on-Kaggle  \n",
       "3                 /What-are-some-alternatives-to-Kaggle  \n",
       "4     /What-Kaggle-competitions-should-a-beginner-st...  \n",
       "...                                                 ...  \n",
       "1161  /unanswered/I-took-Andrew-Ngs-course-on-ML-and...  \n",
       "1162  /What-are-the-various-online-programming-contests  \n",
       "1163     /What-are-some-good-resources-for-learning-R-1  \n",
       "1164  /What-is-the-best-way-to-start-learning-R-I-wo...  \n",
       "1165  /What-are-all-the-companies-that-Google-has-ev...  \n",
       "\n",
       "[1166 rows x 4 columns]"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = pd.read_csv(\"data/QA数据集.csv\")\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f37d7846-795d-44c8-a428-911fff0f69fd",
   "metadata": {},
   "source": [
    "针对上面数据集我们的做法是：\n",
    "1. 把Questions作为问题，把Link作为答案\n",
    "2. 对每一个问题计算Embedding，然后存储起来\n",
    "3. 利用Embedding从存储的地方检索最相似的Question，返回对应的答案"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "2e4be203-9775-4ce2-84de-25ed47ddc00c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from openai.embeddings_utils import get_embedding, cosine_similarity\n",
    "import openai\n",
    "import numpy as np\n",
    "import os\n",
    "openai.api_key = os.environ.get(\"OPENAI_API_KEY\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "63d248fa-d923-4411-999c-097e6c4e1caf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 采用list模拟问答数据库\n",
    "vec_base = []\n",
    "# 遍历dataframe，计算每个问题的 embedding（取前5）\n",
    "for v in df[:5].itertuples():\n",
    "    emb = get_embedding(v.Questions)\n",
    "    im = {\n",
    "        \"question\": v.Questions,\n",
    "        \"embedding\": emb,\n",
    "        \"answer\": v.Link\n",
    "    }\n",
    "    vec_base.append(im)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "71066190-dfa1-4d2c-af8e-ed7c1b4745ef",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[0.6657692047665936,\n",
       " 0.8711775410642534,\n",
       " 0.7489853201153619,\n",
       " 0.7384357684745505,\n",
       " 0.728712915398222]"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 模拟一个新的问题\n",
    "query = \"is kaggle alive?\"\n",
    "# 获取问题的embedding\n",
    "q_emb = get_embedding(query)\n",
    "# 遍历模拟的数据库，计算上述问题在数据库中的每一个相似度\n",
    "sims = [cosine_similarity(q_emb, v[\"embedding\"]) for v in vec_base]\n",
    "sims"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a07a170a-eb56-4717-8384-99394a84f1d2",
   "metadata": {},
   "source": [
    "由上述相似度可看出，直接选择第二个问题即可："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4d59a3ed-272b-438c-87de-8070edccdd7f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Is Kaggle dead?', '/Is-Kaggle-dead')"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "vec_base[1][\"question\"], vec_base[1][\"answer\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d01dbd4b-c9c2-43ae-87ab-18762428a252",
   "metadata": {},
   "source": [
    "当然，在实际中，我们不建议使用循环，大家可以使用NumPy进行批量计算。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8957f1d1-45dd-471c-8318-503520d0501d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(5, 12288)"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 获取数据库的embedding集合\n",
    "arr = np.array(\n",
    "    [v[\"embedding\"] for v in vec_base]\n",
    ")\n",
    "arr.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "830ace61-606d-4d4e-ab82-7abfae804702",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1, 12288)"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 获取问题的embedding\n",
    "q_arr = np.expand_dims(q_emb, 0)\n",
    "q_arr.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "2c8a944b-f929-480d-94b3-e9b8be88ebba",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0.6657692 ],\n",
       "       [0.87117754],\n",
       "       [0.74898532],\n",
       "       [0.73843577],\n",
       "       [0.72871292]])"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.metrics.pairwise import cosine_similarity\n",
    "\n",
    "# 批量计算相似度\n",
    "cosine_similarity(arr, q_arr)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c61eacd4-0ae1-4142-9a12-91740d101bb6",
   "metadata": {},
   "source": [
    "&emsp;&emsp;不过，当Question非常多，比如上百万甚至上亿时，这种方式就不合适了。一个是内存里可能放不下，另一个是算起来也很慢。这时候就必须借助一些专门用来做语义检索的工具了。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "375142e4-1a6e-470f-8423-4002a58bca01",
   "metadata": {},
   "source": [
    "&emsp;&emsp;比较常用的工具有：\n",
    "\n",
    "- [facebookresearch/faiss: A library for efficient similarity search and clustering of dense vectors.](https://github.com/facebookresearch/faiss)\n",
    "- [milvus-io/milvus: Vector database for scalable similarity search and AI applications.](https://github.com/milvus-io/milvus)\n",
    "- [Vector similarity | Redis](https://redis.io/docs/stack/search/reference/vectors/)\n",
    "\n",
    "此处，我们以Redis为例，其他工具用法类似。步骤如下：\n",
    "1. 启动一个Redis服务\n",
    "2. 安装python的Redis库\n",
    "```shell\n",
    "pip install redis\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "70118146-f3a6-40d8-b6c1-2d2fe1ba779b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "b'hello!'\n"
     ]
    }
   ],
   "source": [
    "# 测试\n",
    "import redis\n",
    "r = redis.Redis()\n",
    "r.set(\"test\", \"hello!\")\n",
    "print(r.get(\"test\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "64852ceb-b8e5-4a68-8793-4064eba18690",
   "metadata": {},
   "source": [
    "当然，实际上使用ElasticSearch可能更为合适一些。</br>\n",
    "如果大家使用过ElasticSearch，接下来的内容会非常容易理解。总的来说，和刚刚的步骤差不多，但是这里我们需要先建索引，然后生成Embedding并把它存储到Redis，再进行使用（从索引中搜索）。不过由于我们使用了工具，具体步骤会略微不同。</br>\n",
    "索引的概念和数据库中的索引有点相似，就是要定义一组Schema，告诉Redis你的字段是什么，有哪些属性。</br>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "7e29e8e5-5121-4426-a384-6e6932a1f187",
   "metadata": {},
   "outputs": [],
   "source": [
    "VECTOR_DIM = 12288\n",
    "INDEX_NAME = \"faq\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "e67394a3-fa6a-4ed9-b536-942b4e68b230",
   "metadata": {},
   "outputs": [],
   "source": [
    "from redis.commands.search.query import Query\n",
    "from redis.commands.search.field import TextField, VectorField"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "2780ef3b-238f-4680-8332-123cbb053a81",
   "metadata": {},
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'ResponseError' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mResponseError\u001b[0m                             Traceback (most recent call last)",
      "Input \u001b[1;32mIn [10]\u001b[0m, in \u001b[0;36m<cell line: 15>\u001b[1;34m()\u001b[0m\n\u001b[0;32m     15\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m---> 16\u001b[0m     info \u001b[38;5;241m=\u001b[39m \u001b[43mindex\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minfo\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m     17\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m ResponseError \u001b[38;5;28;01mas\u001b[39;00m ex:\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\redis\\commands\\search\\commands.py:370\u001b[0m, in \u001b[0;36mSearchCommands.info\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    363\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m    364\u001b[0m \u001b[38;5;124;03mGet info an stats about the the current index, including the number of\u001b[39;00m\n\u001b[0;32m    365\u001b[0m \u001b[38;5;124;03mdocuments, memory consumption, etc\u001b[39;00m\n\u001b[0;32m    366\u001b[0m \n\u001b[0;32m    367\u001b[0m \u001b[38;5;124;03mFor more information see `FT.INFO <https://redis.io/commands/ft.info>`_.\u001b[39;00m\n\u001b[0;32m    368\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m--> 370\u001b[0m res \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute_command\u001b[49m\u001b[43m(\u001b[49m\u001b[43mINFO_CMD\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindex_name\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    371\u001b[0m it \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mmap\u001b[39m(to_string, res)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\redis\\client.py:1269\u001b[0m, in \u001b[0;36mRedis.execute_command\u001b[1;34m(self, *args, **options)\u001b[0m\n\u001b[0;32m   1268\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m-> 1269\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mconn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mretry\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcall_with_retry\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m   1270\u001b[0m \u001b[43m        \u001b[49m\u001b[38;5;28;43;01mlambda\u001b[39;49;00m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_send_command_parse_response\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m   1271\u001b[0m \u001b[43m            \u001b[49m\u001b[43mconn\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcommand_name\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43moptions\u001b[49m\n\u001b[0;32m   1272\u001b[0m \u001b[43m        \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m   1273\u001b[0m \u001b[43m        \u001b[49m\u001b[38;5;28;43;01mlambda\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43merror\u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_disconnect_raise\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconn\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43merror\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m   1274\u001b[0m \u001b[43m    \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m   1275\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\redis\\retry.py:46\u001b[0m, in \u001b[0;36mRetry.call_with_retry\u001b[1;34m(self, do, fail)\u001b[0m\n\u001b[0;32m     45\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m---> 46\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mdo\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m     47\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_supported_errors \u001b[38;5;28;01mas\u001b[39;00m error:\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\redis\\client.py:1270\u001b[0m, in \u001b[0;36mRedis.execute_command.<locals>.<lambda>\u001b[1;34m()\u001b[0m\n\u001b[0;32m   1268\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m   1269\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m conn\u001b[38;5;241m.\u001b[39mretry\u001b[38;5;241m.\u001b[39mcall_with_retry(\n\u001b[1;32m-> 1270\u001b[0m         \u001b[38;5;28;01mlambda\u001b[39;00m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_send_command_parse_response(\n\u001b[0;32m   1271\u001b[0m             conn, command_name, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39moptions\n\u001b[0;32m   1272\u001b[0m         ),\n\u001b[0;32m   1273\u001b[0m         \u001b[38;5;28;01mlambda\u001b[39;00m error: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_disconnect_raise(conn, error),\n\u001b[0;32m   1274\u001b[0m     )\n\u001b[0;32m   1275\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\redis\\client.py:1246\u001b[0m, in \u001b[0;36mRedis._send_command_parse_response\u001b[1;34m(self, conn, command_name, *args, **options)\u001b[0m\n\u001b[0;32m   1245\u001b[0m conn\u001b[38;5;241m.\u001b[39msend_command(\u001b[38;5;241m*\u001b[39margs)\n\u001b[1;32m-> 1246\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparse_response(conn, command_name, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39moptions)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\redis\\client.py:1286\u001b[0m, in \u001b[0;36mRedis.parse_response\u001b[1;34m(self, connection, command_name, **options)\u001b[0m\n\u001b[0;32m   1285\u001b[0m     \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m-> 1286\u001b[0m         response \u001b[38;5;241m=\u001b[39m \u001b[43mconnection\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_response\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m   1287\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m ResponseError:\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\redis\\connection.py:897\u001b[0m, in \u001b[0;36mAbstractConnection.read_response\u001b[1;34m(self, disable_decoding, disconnect_on_error)\u001b[0m\n\u001b[0;32m    896\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(response, ResponseError):\n\u001b[1;32m--> 897\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m response\n\u001b[0;32m    898\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m response\n",
      "\u001b[1;31mResponseError\u001b[0m: unknown command 'FT.INFO'",
      "\nDuring handling of the above exception, another exception occurred:\n",
      "\u001b[1;31mNameError\u001b[0m                                 Traceback (most recent call last)",
      "Input \u001b[1;32mIn [10]\u001b[0m, in \u001b[0;36m<cell line: 15>\u001b[1;34m()\u001b[0m\n\u001b[0;32m     15\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m     16\u001b[0m     info \u001b[38;5;241m=\u001b[39m index\u001b[38;5;241m.\u001b[39minfo()\n\u001b[1;32m---> 17\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[43mResponseError\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m ex:\n\u001b[0;32m     18\u001b[0m     index\u001b[38;5;241m.\u001b[39mcreate_index(schema)\n",
      "\u001b[1;31mNameError\u001b[0m: name 'ResponseError' is not defined"
     ]
    }
   ],
   "source": [
    "# 建好要存字段的索引，针对不同属性字段，使用不同Field\n",
    "question = TextField(name=\"question\")\n",
    "answer = TextField(name=\"answer\")\n",
    "embedding = VectorField(\n",
    "    name=\"embedding\", \n",
    "    algorithm=\"HNSW\", \n",
    "    attributes={\n",
    "        \"TYPE\": \"FLOAT32\",\n",
    "        \"DIM\": VECTOR_DIM,\n",
    "        \"DISTANCE_METRIC\": \"COSINE\"\n",
    "    }\n",
    ")\n",
    "schema = (question, embedding, answer)\n",
    "index = r.ft(INDEX_NAME)\n",
    "try:\n",
    "    info = index.info()\n",
    "except Exception as ex:W\n",
    "    index.create_index(schema)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "304f461b-c3e0-4a47-a821-e10b3e0a4d7a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 如果需要删除已有文档的话，可以使用下面的命令\n",
    "index.dropindex(delete_documents=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5ecbf63e-ff80-4d74-859b-17a62355d3e7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 遍历前5数据，获取embedding并写入redis\n",
    "for v in df[:5].itertuples():\n",
    "    emb = get_embedding(v.Questions)\n",
    "    # 注意，redis要存储bytes或string\n",
    "    emb = np.array(emb, dtype=np.float32).tobytes()\n",
    "    im = {\n",
    "        \"question\": v.Questions,\n",
    "        \"embedding\": emb,\n",
    "        \"answer\": v.Link\n",
    "    }\n",
    "    # 重点是这句\n",
    "    r.hset(name=f\"{INDEX_NAME}-{v.Index}\", mapping=im)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3b5c318d-5716-4908-a464-c5595b19b53c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 构造查询输入\n",
    "query = \"kaggle alive?\"\n",
    "embed_query = get_embedding(query)\n",
    "params_dict = {\"query_embedding\": np.array(embed_query).astype(dtype=np.float32).tobytes()}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11c93eb1-9124-408c-96d2-f9e4716147ec",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 使用Redis的knn算法对进行相似度计算， 返回top 3的结果\n",
    "k = 3\n",
    "base_query = f\"* => [KNN {k} @embedding $query_embedding AS similarity]\"\n",
    "return_fields = [\"question\", \"answer\", \"similarity\"]\n",
    "query = (\n",
    "    Query(base_query)\n",
    "     .return_fields(*return_fields)\n",
    "     .sort_by(\"similarity\")\n",
    "     .paging(0, k)\n",
    "     .dialect(2)\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0bf0abbe-17d2-40c4-8094-335963946464",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 查询\n",
    "res = index.search(query, params_dict)\n",
    "for i,doc in enumerate(res.docs):\n",
    "    score = 1 - float(doc.similarity)\n",
    "    print(f\"{doc.id}, {doc.question}, {doc.answer} (Score: {round(score ,3) })\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d12a2cc8-5fe8-44c7-ad23-b1da810791d1",
   "metadata": {
    "jp-MarkdownHeadingCollapsed": true,
    "tags": []
   },
   "source": [
    "## 聚类"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59f6a41b-19c2-4b0b-a898-f158c3c4021f",
   "metadata": {},
   "source": [
    "聚类的意思是把彼此相近的样本聚集在一起，本质也是在使用一种表示和相似度衡量来处理文本。比如我们有大量的未分类文本，如果事先能知道有几种类别，就可以用聚类的方法先将样本大致分一下。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e09d445-6a27-4d7b-baa8-a43ad05471a3",
   "metadata": {},
   "source": [
    "我们使用Kaggle的DBPedia数据集：[DBPedia Classes | Kaggle](https://www.kaggle.com/datasets/danofer/dbpedia-classes?select=DBPEDIA_val.csv)。\n",
    "\n",
    "这个数据集对一段文本会给出三个不同层次级别的分类标签，我们这里用第一层的类别。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "5c63845f-1c73-431a-a5d6-aca3a9b50761",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>text</th>\n",
       "      <th>l1</th>\n",
       "      <th>l2</th>\n",
       "      <th>l3</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Li Curt is a station on the Bernina Railway li...</td>\n",
       "      <td>Place</td>\n",
       "      <td>Station</td>\n",
       "      <td>RailwayStation</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Grafton State Hospital was a psychiatric hospi...</td>\n",
       "      <td>Place</td>\n",
       "      <td>Building</td>\n",
       "      <td>Hospital</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>The Democratic Patriotic Alliance of Kurdistan...</td>\n",
       "      <td>Agent</td>\n",
       "      <td>Organisation</td>\n",
       "      <td>PoliticalParty</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Ira Rakatansky (October 3, 1919 – March 4, 201...</td>\n",
       "      <td>Agent</td>\n",
       "      <td>Person</td>\n",
       "      <td>Architect</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Universitatea Reșița is a women handball club ...</td>\n",
       "      <td>Agent</td>\n",
       "      <td>SportsTeam</td>\n",
       "      <td>HandballTeam</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>35998</th>\n",
       "      <td>The Great Pershing Balloon Derby is a hot air ...</td>\n",
       "      <td>Event</td>\n",
       "      <td>SocietalEvent</td>\n",
       "      <td>Convention</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>35999</th>\n",
       "      <td>Microsystems was a personal computing magazine...</td>\n",
       "      <td>Work</td>\n",
       "      <td>PeriodicalLiterature</td>\n",
       "      <td>Magazine</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>36000</th>\n",
       "      <td>The 1899 Open Championship was the 39th Open C...</td>\n",
       "      <td>Event</td>\n",
       "      <td>Tournament</td>\n",
       "      <td>GolfTournament</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>36001</th>\n",
       "      <td>Kristina Repelewska (born 7 January 1981) is a...</td>\n",
       "      <td>Agent</td>\n",
       "      <td>Athlete</td>\n",
       "      <td>HandballPlayer</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>36002</th>\n",
       "      <td>Lamhara is a small town and rural commune in T...</td>\n",
       "      <td>Place</td>\n",
       "      <td>Settlement</td>\n",
       "      <td>Town</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>36003 rows × 4 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                    text     l1  \\\n",
       "0      Li Curt is a station on the Bernina Railway li...  Place   \n",
       "1      Grafton State Hospital was a psychiatric hospi...  Place   \n",
       "2      The Democratic Patriotic Alliance of Kurdistan...  Agent   \n",
       "3      Ira Rakatansky (October 3, 1919 – March 4, 201...  Agent   \n",
       "4      Universitatea Reșița is a women handball club ...  Agent   \n",
       "...                                                  ...    ...   \n",
       "35998  The Great Pershing Balloon Derby is a hot air ...  Event   \n",
       "35999  Microsystems was a personal computing magazine...   Work   \n",
       "36000  The 1899 Open Championship was the 39th Open C...  Event   \n",
       "36001  Kristina Repelewska (born 7 January 1981) is a...  Agent   \n",
       "36002  Lamhara is a small town and rural commune in T...  Place   \n",
       "\n",
       "                         l2              l3  \n",
       "0                   Station  RailwayStation  \n",
       "1                  Building        Hospital  \n",
       "2              Organisation  PoliticalParty  \n",
       "3                    Person       Architect  \n",
       "4                SportsTeam    HandballTeam  \n",
       "...                     ...             ...  \n",
       "35998         SocietalEvent      Convention  \n",
       "35999  PeriodicalLiterature        Magazine  \n",
       "36000            Tournament  GolfTournament  \n",
       "36001               Athlete  HandballPlayer  \n",
       "36002            Settlement            Town  \n",
       "\n",
       "[36003 rows x 4 columns]"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import pandas as pd\n",
    "cdf = pd.read_csv(\"data/聚类数据集.csv\")\n",
    "cdf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "34b2e633-95fc-46ba-80d0-067267eb1e89",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 数据类别分布情况\n",
    "cdf['l1'].value_counts()\n",
    "\n",
    "cdf = cdf[:10]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "16033950-8b17-420b-8f72-5e568e6595e7",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "10it [00:04,  2.16it/s]\n",
      "C:\\Users\\turke\\AppData\\Local\\Temp\\ipykernel_21536\\3003042142.py:12: SettingWithCopyWarning: \n",
      "A value is trying to be set on a copy of a slice from a DataFrame.\n",
      "Try using .loc[row_indexer,col_indexer] = value instead\n",
      "\n",
      "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
      "  cdf[\"embedding\"] = embed_list\n"
     ]
    }
   ],
   "source": [
    "# 再次调用ChatGPT,获取文本的embedding\n",
    "from openai.embeddings_utils import get_embedding, cosine_similarity\n",
    "import openai\n",
    "import numpy as np\n",
    "import os\n",
    "from tqdm import tqdm \n",
    "openai.api_key = os.environ.get(\"OPENAI_API_KEY\") # 填入api key\n",
    "\n",
    "embed_list = []\n",
    "for idx, row in tqdm(cdf.iterrows()):\n",
    "    embed_list.append(get_embedding(row['text'], engine=\"text-embedding-ada-002\"))\n",
    "cdf[\"embedding\"] = embed_list"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58d27e19-7942-4ea2-82f1-6354e10cfe45",
   "metadata": {},
   "source": [
    "由于返回的embedding维度过高，这里使用PCA降维，把文本用更低维的向量表示"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "49998346-6e9c-417e-8e4f-478573326848",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\turke\\AppData\\Local\\Temp\\ipykernel_21536\\4133867377.py:6: SettingWithCopyWarning: \n",
      "A value is trying to be set on a copy of a slice from a DataFrame.\n",
      "Try using .loc[row_indexer,col_indexer] = value instead\n",
      "\n",
      "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
      "  cdf[\"embed_vis\"] = vis_dims.tolist()\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "((10, 1536), (10, 3))"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# PCA降维\n",
    "from sklearn.decomposition import PCA\n",
    "arr = np.array(cdf.embedding.to_list())\n",
    "pca = PCA(n_components=3)\n",
    "vis_dims = pca.fit_transform(arr)\n",
    "cdf[\"embed_vis\"] = vis_dims.tolist()\n",
    "arr.shape, vis_dims.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "5d59c751-1d17-453b-9d24-43f6d98c71cb",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 简单画个图\n",
    "%matplotlib inline\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "\n",
    "fig, ax = plt.subplots(subplot_kw={\"projection\": \"3d\"}, figsize=(8, 8))\n",
    "cmap = plt.get_cmap(\"tab20\")\n",
    "categories = sorted(cdf.l1.unique())\n",
    "\n",
    "# 分别绘制每个类别\n",
    "for i, cat in enumerate(categories):\n",
    "    sub_matrix = np.array(cdf[cdf.l1 == cat][\"embed_vis\"].to_list())\n",
    "    x=sub_matrix[:, 0]\n",
    "    y=sub_matrix[:, 1]\n",
    "    z=sub_matrix[:, 2]\n",
    "    colors = [cmap(i/len(categories))] * len(sub_matrix)\n",
    "    ax.scatter(x, y, z, c=colors, label=cat)\n",
    "\n",
    "ax.legend(bbox_to_anchor=(1.2, 1))\n",
    "plt.show();"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dad798c7-16c0-4010-b001-f4c1ccb59fcc",
   "metadata": {},
   "source": [
    "从上图就能得到，明显的不同分类在空间向量中的分布"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a315fd0-5241-4c41-ad79-7e8925234750",
   "metadata": {
    "jp-MarkdownHeadingCollapsed": true,
    "tags": []
   },
   "source": [
    "## 推荐"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "afb84bdd-a643-4f5b-a644-b7a3bd912cd1",
   "metadata": {},
   "source": [
    "很多APP或网站上都能看到推荐功能。比如在购物网站，每当你登陆或者选购一件商品后，系统就会给你推荐一些相关的产品。由于缺少商品类数据，而且ChatGPT属于文本类任务，所以这里推进文本，比如帖子、文章、新闻等。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b031f622-88a7-40ff-8d5e-892049dd47c4",
   "metadata": {},
   "source": [
    "我们以新闻为例，先说一下基本逻辑：\n",
    "\n",
    "1. 首先要有一个基础的文章库，可能包括标题、内容、标签等。\n",
    "2. 计算已有文章的Embedding并存储。\n",
    "3. 根据用户浏览记录，推荐和浏览记录最相似的文章。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a787e77-71c2-4c72-8afc-7f0a5d98467e",
   "metadata": {},
   "source": [
    "使用如下的数据集：[AG News Classification Dataset | Kaggle](https://www.kaggle.com/datasets/amananandrai/ag-news-classification-dataset?select=train.csv)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "93b5c0cf-4741-40aa-b000-418d3d30e13d",
   "metadata": {},
   "source": [
    "看起来好像和前面的QA差不多，事实也的确如此，因为它们本质上都是相似匹配问题。只不过QA使用的是用户的Question去匹配已有知识库，而推荐是使用用户的浏览记录去匹配。但是很明显，推荐相比QA要更复杂一些，主要包括以下几个方面：\n",
    "\n",
    "- 刚开始用户没有记录时的推荐（一般行业称为冷启动问题）。\n",
    "- 除了相似还有其他要考虑的因素：比如热门内容、新内容、内容多样性、随时间变化的兴趣变化等等。\n",
    "- 编码（Embedding输入）问题：我们应该取标题呢，还是文章，还是简要描述或者摘要，还是都要计算。\n",
    "- 规模问题：推荐面临的量级一般会远超QA，除了横向扩展机器，是否能从流程和算法设计上提升效率。\n",
    "- 用户反馈对推荐系统的影响问题：用户反感或喜欢与文章本身并没有直接关系，比如用户喜欢体育新闻但讨厌中国足球。\n",
    "- 线上实时更新问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "532427af-19c4-4b04-bf21-47f37d3ff0d5",
   "metadata": {},
   "source": [
    "我们这里综合考虑上面的因素给大家一个比较简单的方案，但务必注意，其中每个模块的方案都不是唯一的。整体设计如下：\n",
    "\n",
    "- 用户注册登录时，让其选择感兴趣的类型（如体育、音乐、时尚等），我们通过这一步将用户框在一个大的范围内，同时用来顺道解决冷启动问题。\n",
    "- 给用户推荐内容时，在知道类别（用户注册时选择+浏览记录）后，应依次考虑时效性、热门程度、多样性等。\n",
    "- 考虑到性能问题，可以编码「标题+摘要」。\n",
    "- 对大类别进一步细分，只在细分类别里进行相似度计算。\n",
    "- 记录用户实时行为（如浏览Item、浏览时长、评论、收藏、点赞、转发等）。\n",
    "- 动态更新内容库，更新用户行为库。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3db81ac7-aa70-464a-bf54-6800abeb9532",
   "metadata": {},
   "source": [
    "在具体实施时，我们使用最常用的流程线方案：召回+排序。\n",
    "\n",
    "- 召回：通过各种不同属性或特征（如用户偏好、热点、行为等）先找到一批要推荐列表。\n",
    "- 排序：根据多样性、时效性、用户反馈、热门程度等属性对结果进行排序。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "3e6e004a-34c8-4d24-a473-aa2063d66b22",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Class Index</th>\n",
       "      <th>Title</th>\n",
       "      <th>Description</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>3</td>\n",
       "      <td>Wall St. Bears Claw Back Into the Black (Reuters)</td>\n",
       "      <td>Reuters - Short-sellers, Wall Street's dwindli...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>3</td>\n",
       "      <td>Carlyle Looks Toward Commercial Aerospace (Reu...</td>\n",
       "      <td>Reuters - Private investment firm Carlyle Grou...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3</td>\n",
       "      <td>Oil and Economy Cloud Stocks' Outlook (Reuters)</td>\n",
       "      <td>Reuters - Soaring crude prices plus worries\\ab...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>3</td>\n",
       "      <td>Iraq Halts Oil Exports from Main Southern Pipe...</td>\n",
       "      <td>Reuters - Authorities have halted oil export\\f...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3</td>\n",
       "      <td>Oil prices soar to all-time record, posing new...</td>\n",
       "      <td>AFP - Tearaway world oil prices, toppling reco...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>119995</th>\n",
       "      <td>1</td>\n",
       "      <td>Pakistan's Musharraf Says Won't Quit as Army C...</td>\n",
       "      <td>KARACHI (Reuters) - Pakistani President Perve...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>119996</th>\n",
       "      <td>2</td>\n",
       "      <td>Renteria signing a top-shelf deal</td>\n",
       "      <td>Red Sox general manager Theo Epstein acknowled...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>119997</th>\n",
       "      <td>2</td>\n",
       "      <td>Saban not going to Dolphins yet</td>\n",
       "      <td>The Miami Dolphins will put their courtship of...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>119998</th>\n",
       "      <td>2</td>\n",
       "      <td>Today's NFL games</td>\n",
       "      <td>PITTSBURGH at NY GIANTS Time: 1:30 p.m. Line: ...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>119999</th>\n",
       "      <td>2</td>\n",
       "      <td>Nets get Carter from Raptors</td>\n",
       "      <td>INDIANAPOLIS -- All-Star Vince Carter was trad...</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>120000 rows × 3 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "        Class Index                                              Title  \\\n",
       "0                 3  Wall St. Bears Claw Back Into the Black (Reuters)   \n",
       "1                 3  Carlyle Looks Toward Commercial Aerospace (Reu...   \n",
       "2                 3    Oil and Economy Cloud Stocks' Outlook (Reuters)   \n",
       "3                 3  Iraq Halts Oil Exports from Main Southern Pipe...   \n",
       "4                 3  Oil prices soar to all-time record, posing new...   \n",
       "...             ...                                                ...   \n",
       "119995            1  Pakistan's Musharraf Says Won't Quit as Army C...   \n",
       "119996            2                  Renteria signing a top-shelf deal   \n",
       "119997            2                    Saban not going to Dolphins yet   \n",
       "119998            2                                  Today's NFL games   \n",
       "119999            2                       Nets get Carter from Raptors   \n",
       "\n",
       "                                              Description  \n",
       "0       Reuters - Short-sellers, Wall Street's dwindli...  \n",
       "1       Reuters - Private investment firm Carlyle Grou...  \n",
       "2       Reuters - Soaring crude prices plus worries\\ab...  \n",
       "3       Reuters - Authorities have halted oil export\\f...  \n",
       "4       AFP - Tearaway world oil prices, toppling reco...  \n",
       "...                                                   ...  \n",
       "119995   KARACHI (Reuters) - Pakistani President Perve...  \n",
       "119996  Red Sox general manager Theo Epstein acknowled...  \n",
       "119997  The Miami Dolphins will put their courtship of...  \n",
       "119998  PITTSBURGH at NY GIANTS Time: 1:30 p.m. Line: ...  \n",
       "119999  INDIANAPOLIS -- All-Star Vince Carter was trad...  \n",
       "\n",
       "[120000 rows x 3 columns]"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from dataclasses import dataclass\n",
    "import pandas as pd\n",
    "df = pd.read_csv(\"data/推荐数据集.csv\")\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "169bf112-3c9a-41c7-9e97-231f7de768f6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "针对数据类别的分类为：\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "3    30000\n",
       "4    30000\n",
       "2    30000\n",
       "1    30000\n",
       "Name: Class Index, dtype: int64"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print('针对数据类别的分类为：')\n",
    "df[\"Class Index\"].value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2aea1338-e96b-4deb-8180-0f8c0da0035b",
   "metadata": {},
   "source": [
    "根据数据集介绍（上面的链接），四个类型分别是：1-World, 2-Sports, 3-Business, 4-Sci/Tech，每个类型有30000条数据，共12万条。接下来，我们将使用上面已经介绍的知识来做一个简单的流水线系统。</br>\n",
    "为了便于运行，依然取100条sample作为示例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "f74a4a04-d46f-44b4-ad1d-f9c793ca213c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2    28\n",
       "3    26\n",
       "1    25\n",
       "4    21\n",
       "Name: Class Index, dtype: int64"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sdf = df.sample(100)\n",
    "sdf[\"Class Index\"].value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d44c0a96-f0ba-4da2-9860-4ffe8fc8e133",
   "metadata": {},
   "source": [
    "&emsp;&emsp;首先维护一个用户偏好和行为记录："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "c6d52863-60f9-45f6-ab71-037b3543296d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import List\n",
    "\n",
    "# 用户对象\n",
    "@dataclass\n",
    "class User:\n",
    "    \n",
    "    user_name: str\n",
    "\n",
    "# 用户爱好\n",
    "@dataclass\n",
    "class UserPrefer:\n",
    "    \n",
    "    user_name: str\n",
    "    prefers: List[int]\n",
    "\n",
    "# 文章\n",
    "@dataclass\n",
    "class Item:\n",
    "    \n",
    "    item_id: str\n",
    "    item_props: dict\n",
    "\n",
    "# 行为\n",
    "@dataclass\n",
    "class Action:\n",
    "    \n",
    "    action_type: str\n",
    "    action_props: dict\n",
    "\n",
    "# 用户行为\n",
    "@dataclass\n",
    "class UserAction:\n",
    "    \n",
    "    user: User\n",
    "    item: Item\n",
    "    action: Action\n",
    "    action_time: str"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "cc41b71d-a877-4af1-9563-7d4da43f979e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 注册用户u1，并设置爱好为1和2\n",
    "u1 = User(\"u1\")\n",
    "up1 = UserPrefer(\"u1\", [1, 2])\n",
    "\n",
    "# 模拟用户历史浏览\n",
    "i1 = Item(\"i1\", {\n",
    "    \"id\": 1, \n",
    "    \"catetory\": \"sport\",\n",
    "    \"title\": \"Swimming: Shibata Joins Japanese Gold Rush\", \n",
    "    \"description\": \"\\\n",
    "    ATHENS (Reuters) - Ai Shibata wore down French teen-ager  Laure Manaudou to win the women's 800 meters \\\n",
    "    freestyle gold  medal at the Athens Olympics Friday and provide Japan with  their first female swimming \\\n",
    "    champion in 12 years.\", \n",
    "    \"content\": \"content\"\n",
    "})\n",
    "a1 = Action(\"浏览\", {\n",
    "    \"open_time\": \"2023-04-01 12:00:00\", \n",
    "    \"leave_time\": \"2023-04-01 14:00:00\",\n",
    "    \"type\": \"close\",\n",
    "    \"duration\": \"2hour\"\n",
    "})\n",
    "ua1 = UserAction(u1, i1, a1, \"2023-04-01 12:00:00\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "a0f5bfd9-2afc-4bbd-a4be-93593d301ed2",
   "metadata": {},
   "outputs": [
    {
     "ename": "KeyboardInterrupt",
     "evalue": "",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mKeyboardInterrupt\u001b[0m                         Traceback (most recent call last)",
      "Input \u001b[1;32mIn [15]\u001b[0m, in \u001b[0;36m<cell line: 9>\u001b[1;34m()\u001b[0m\n\u001b[0;32m      5\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnp\u001b[39;00m\n\u001b[0;32m      7\u001b[0m openai\u001b[38;5;241m.\u001b[39mapi_key \u001b[38;5;241m=\u001b[39m os\u001b[38;5;241m.\u001b[39menviron\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mOPENAI_API_KEY\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;66;03m# 填入api key\u001b[39;00m\n\u001b[1;32m----> 9\u001b[0m sdf[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124membedding\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[43msdf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43;01mlambda\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mx\u001b[49m\u001b[43m:\u001b[49m\u001b[43m  \u001b[49m\u001b[43mget_embedding\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mTitle\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mx\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mDescription\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mengine\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtext-embedding-ada-002\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\pandas\\core\\frame.py:8839\u001b[0m, in \u001b[0;36mDataFrame.apply\u001b[1;34m(self, func, axis, raw, result_type, args, **kwargs)\u001b[0m\n\u001b[0;32m   8828\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mapply\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m frame_apply\n\u001b[0;32m   8830\u001b[0m op \u001b[38;5;241m=\u001b[39m frame_apply(\n\u001b[0;32m   8831\u001b[0m     \u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m   8832\u001b[0m     func\u001b[38;5;241m=\u001b[39mfunc,\n\u001b[1;32m   (...)\u001b[0m\n\u001b[0;32m   8837\u001b[0m     kwargs\u001b[38;5;241m=\u001b[39mkwargs,\n\u001b[0;32m   8838\u001b[0m )\n\u001b[1;32m-> 8839\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mop\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39m__finalize__(\u001b[38;5;28mself\u001b[39m, method\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mapply\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\pandas\\core\\apply.py:727\u001b[0m, in \u001b[0;36mFrameApply.apply\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    724\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mraw:\n\u001b[0;32m    725\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapply_raw()\n\u001b[1;32m--> 727\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply_standard\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\pandas\\core\\apply.py:851\u001b[0m, in \u001b[0;36mFrameApply.apply_standard\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    850\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mapply_standard\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m--> 851\u001b[0m     results, res_index \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply_series_generator\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    853\u001b[0m     \u001b[38;5;66;03m# wrap results\u001b[39;00m\n\u001b[0;32m    854\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mwrap_results(results, res_index)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\pandas\\core\\apply.py:867\u001b[0m, in \u001b[0;36mFrameApply.apply_series_generator\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    864\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m option_context(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmode.chained_assignment\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[0;32m    865\u001b[0m     \u001b[38;5;28;01mfor\u001b[39;00m i, v \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(series_gen):\n\u001b[0;32m    866\u001b[0m         \u001b[38;5;66;03m# ignore SettingWithCopy here in case the user mutates\u001b[39;00m\n\u001b[1;32m--> 867\u001b[0m         results[i] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[43mv\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    868\u001b[0m         \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(results[i], ABCSeries):\n\u001b[0;32m    869\u001b[0m             \u001b[38;5;66;03m# If we have a view on v, we need to make a copy because\u001b[39;00m\n\u001b[0;32m    870\u001b[0m             \u001b[38;5;66;03m#  series_generator will swap out the underlying data\u001b[39;00m\n\u001b[0;32m    871\u001b[0m             results[i] \u001b[38;5;241m=\u001b[39m results[i]\u001b[38;5;241m.\u001b[39mcopy(deep\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m)\n",
      "Input \u001b[1;32mIn [15]\u001b[0m, in \u001b[0;36m<lambda>\u001b[1;34m(x)\u001b[0m\n\u001b[0;32m      5\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnp\u001b[39;00m\n\u001b[0;32m      7\u001b[0m openai\u001b[38;5;241m.\u001b[39mapi_key \u001b[38;5;241m=\u001b[39m os\u001b[38;5;241m.\u001b[39menviron\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mOPENAI_API_KEY\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;66;03m# 填入api key\u001b[39;00m\n\u001b[1;32m----> 9\u001b[0m sdf[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124membedding\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m sdf\u001b[38;5;241m.\u001b[39mapply(\u001b[38;5;28;01mlambda\u001b[39;00m x:  \u001b[43mget_embedding\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mTitle\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mx\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mDescription\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mengine\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtext-embedding-ada-002\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m, axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\tenacity\\__init__.py:324\u001b[0m, in \u001b[0;36mBaseRetrying.wraps.<locals>.wrapped_f\u001b[1;34m(*args, **kw)\u001b[0m\n\u001b[0;32m    322\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mwraps(f)\n\u001b[0;32m    323\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrapped_f\u001b[39m(\u001b[38;5;241m*\u001b[39margs: t\u001b[38;5;241m.\u001b[39mAny, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkw: t\u001b[38;5;241m.\u001b[39mAny) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m t\u001b[38;5;241m.\u001b[39mAny:\n\u001b[1;32m--> 324\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m(f, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkw)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\tenacity\\__init__.py:404\u001b[0m, in \u001b[0;36mRetrying.__call__\u001b[1;34m(self, fn, *args, **kwargs)\u001b[0m\n\u001b[0;32m    402\u001b[0m retry_state \u001b[38;5;241m=\u001b[39m RetryCallState(retry_object\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m, fn\u001b[38;5;241m=\u001b[39mfn, args\u001b[38;5;241m=\u001b[39margs, kwargs\u001b[38;5;241m=\u001b[39mkwargs)\n\u001b[0;32m    403\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[1;32m--> 404\u001b[0m     do \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43miter\u001b[49m\u001b[43m(\u001b[49m\u001b[43mretry_state\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mretry_state\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    405\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(do, DoAttempt):\n\u001b[0;32m    406\u001b[0m         \u001b[38;5;28;01mtry\u001b[39;00m:\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\tenacity\\__init__.py:349\u001b[0m, in \u001b[0;36mBaseRetrying.iter\u001b[1;34m(self, retry_state)\u001b[0m\n\u001b[0;32m    347\u001b[0m is_explicit_retry \u001b[38;5;241m=\u001b[39m retry_state\u001b[38;5;241m.\u001b[39moutcome\u001b[38;5;241m.\u001b[39mfailed \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(retry_state\u001b[38;5;241m.\u001b[39moutcome\u001b[38;5;241m.\u001b[39mexception(), TryAgain)\n\u001b[0;32m    348\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (is_explicit_retry \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mretry(retry_state\u001b[38;5;241m=\u001b[39mretry_state)):\n\u001b[1;32m--> 349\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfut\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mresult\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    351\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mafter \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m    352\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mafter(retry_state)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\concurrent\\futures\\_base.py:439\u001b[0m, in \u001b[0;36mFuture.result\u001b[1;34m(self, timeout)\u001b[0m\n\u001b[0;32m    437\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m CancelledError()\n\u001b[0;32m    438\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_state \u001b[38;5;241m==\u001b[39m FINISHED:\n\u001b[1;32m--> 439\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__get_result\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    441\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_condition\u001b[38;5;241m.\u001b[39mwait(timeout)\n\u001b[0;32m    443\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_state \u001b[38;5;129;01min\u001b[39;00m [CANCELLED, CANCELLED_AND_NOTIFIED]:\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\concurrent\\futures\\_base.py:391\u001b[0m, in \u001b[0;36mFuture.__get_result\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    389\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_exception:\n\u001b[0;32m    390\u001b[0m     \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 391\u001b[0m         \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_exception\n\u001b[0;32m    392\u001b[0m     \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[0;32m    393\u001b[0m         \u001b[38;5;66;03m# Break a reference cycle with the exception in self._exception\u001b[39;00m\n\u001b[0;32m    394\u001b[0m         \u001b[38;5;28mself\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\tenacity\\__init__.py:407\u001b[0m, in \u001b[0;36mRetrying.__call__\u001b[1;34m(self, fn, *args, **kwargs)\u001b[0m\n\u001b[0;32m    405\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(do, DoAttempt):\n\u001b[0;32m    406\u001b[0m     \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 407\u001b[0m         result \u001b[38;5;241m=\u001b[39m fn(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[0;32m    408\u001b[0m     \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m:  \u001b[38;5;66;03m# noqa: B902\u001b[39;00m\n\u001b[0;32m    409\u001b[0m         retry_state\u001b[38;5;241m.\u001b[39mset_exception(sys\u001b[38;5;241m.\u001b[39mexc_info())\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\openai\\embeddings_utils.py:23\u001b[0m, in \u001b[0;36mget_embedding\u001b[1;34m(text, engine)\u001b[0m\n\u001b[0;32m     17\u001b[0m \u001b[38;5;129m@retry\u001b[39m(wait\u001b[38;5;241m=\u001b[39mwait_random_exponential(\u001b[38;5;28mmin\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m, \u001b[38;5;28mmax\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m20\u001b[39m), stop\u001b[38;5;241m=\u001b[39mstop_after_attempt(\u001b[38;5;241m6\u001b[39m))\n\u001b[0;32m     18\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget_embedding\u001b[39m(text: \u001b[38;5;28mstr\u001b[39m, engine\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtext-similarity-davinci-001\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m List[\u001b[38;5;28mfloat\u001b[39m]:\n\u001b[0;32m     19\u001b[0m \n\u001b[0;32m     20\u001b[0m     \u001b[38;5;66;03m# replace newlines, which can negatively affect performance.\u001b[39;00m\n\u001b[0;32m     21\u001b[0m     text \u001b[38;5;241m=\u001b[39m text\u001b[38;5;241m.\u001b[39mreplace(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m \u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m---> 23\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mopenai\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mEmbedding\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcreate\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43mtext\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mengine\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mengine\u001b[49m\u001b[43m)\u001b[49m[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mdata\u001b[39m\u001b[38;5;124m\"\u001b[39m][\u001b[38;5;241m0\u001b[39m][\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124membedding\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\openai\\api_resources\\embedding.py:33\u001b[0m, in \u001b[0;36mEmbedding.create\u001b[1;34m(cls, *args, **kwargs)\u001b[0m\n\u001b[0;32m     31\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m     32\u001b[0m     \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m---> 33\u001b[0m         response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mcreate(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[0;32m     35\u001b[0m         \u001b[38;5;66;03m# If a user specifies base64, we'll just return the encoded string.\u001b[39;00m\n\u001b[0;32m     36\u001b[0m         \u001b[38;5;66;03m# This is only for the default case.\u001b[39;00m\n\u001b[0;32m     37\u001b[0m         \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m user_provided_encoding_format:\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\openai\\api_resources\\abstract\\engine_api_resource.py:153\u001b[0m, in \u001b[0;36mEngineAPIResource.create\u001b[1;34m(cls, api_key, api_base, api_type, request_id, api_version, organization, **params)\u001b[0m\n\u001b[0;32m    127\u001b[0m \u001b[38;5;129m@classmethod\u001b[39m\n\u001b[0;32m    128\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcreate\u001b[39m(\n\u001b[0;32m    129\u001b[0m     \u001b[38;5;28mcls\u001b[39m,\n\u001b[1;32m   (...)\u001b[0m\n\u001b[0;32m    136\u001b[0m     \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mparams,\n\u001b[0;32m    137\u001b[0m ):\n\u001b[0;32m    138\u001b[0m     (\n\u001b[0;32m    139\u001b[0m         deployment_id,\n\u001b[0;32m    140\u001b[0m         engine,\n\u001b[1;32m   (...)\u001b[0m\n\u001b[0;32m    150\u001b[0m         api_key, api_base, api_type, api_version, organization, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mparams\n\u001b[0;32m    151\u001b[0m     )\n\u001b[1;32m--> 153\u001b[0m     response, _, api_key \u001b[38;5;241m=\u001b[39m \u001b[43mrequestor\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m    154\u001b[0m \u001b[43m        \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mpost\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m    155\u001b[0m \u001b[43m        \u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    156\u001b[0m \u001b[43m        \u001b[49m\u001b[43mparams\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mparams\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    157\u001b[0m \u001b[43m        \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    158\u001b[0m \u001b[43m        \u001b[49m\u001b[43mstream\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    159\u001b[0m \u001b[43m        \u001b[49m\u001b[43mrequest_id\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest_id\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    160\u001b[0m \u001b[43m        \u001b[49m\u001b[43mrequest_timeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest_timeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    161\u001b[0m \u001b[43m    \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    163\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m stream:\n\u001b[0;32m    164\u001b[0m         \u001b[38;5;66;03m# must be an iterator\u001b[39;00m\n\u001b[0;32m    165\u001b[0m         \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(response, OpenAIResponse)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\openai\\api_requestor.py:216\u001b[0m, in \u001b[0;36mAPIRequestor.request\u001b[1;34m(self, method, url, params, headers, files, stream, request_id, request_timeout)\u001b[0m\n\u001b[0;32m    205\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrequest\u001b[39m(\n\u001b[0;32m    206\u001b[0m     \u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m    207\u001b[0m     method,\n\u001b[1;32m   (...)\u001b[0m\n\u001b[0;32m    214\u001b[0m     request_timeout: Optional[Union[\u001b[38;5;28mfloat\u001b[39m, Tuple[\u001b[38;5;28mfloat\u001b[39m, \u001b[38;5;28mfloat\u001b[39m]]] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[0;32m    215\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Tuple[Union[OpenAIResponse, Iterator[OpenAIResponse]], \u001b[38;5;28mbool\u001b[39m, \u001b[38;5;28mstr\u001b[39m]:\n\u001b[1;32m--> 216\u001b[0m     result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest_raw\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m    217\u001b[0m \u001b[43m        \u001b[49m\u001b[43mmethod\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mlower\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    218\u001b[0m \u001b[43m        \u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    219\u001b[0m \u001b[43m        \u001b[49m\u001b[43mparams\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mparams\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    220\u001b[0m \u001b[43m        \u001b[49m\u001b[43msupplied_headers\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    221\u001b[0m \u001b[43m        \u001b[49m\u001b[43mfiles\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfiles\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    222\u001b[0m \u001b[43m        \u001b[49m\u001b[43mstream\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    223\u001b[0m \u001b[43m        \u001b[49m\u001b[43mrequest_id\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest_id\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    224\u001b[0m \u001b[43m        \u001b[49m\u001b[43mrequest_timeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest_timeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    225\u001b[0m \u001b[43m    \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    226\u001b[0m     resp, got_stream \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_interpret_response(result, stream)\n\u001b[0;32m    227\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m resp, got_stream, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapi_key\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\openai\\api_requestor.py:516\u001b[0m, in \u001b[0;36mAPIRequestor.request_raw\u001b[1;34m(self, method, url, params, supplied_headers, files, stream, request_id, request_timeout)\u001b[0m\n\u001b[0;32m    514\u001b[0m     _thread_context\u001b[38;5;241m.\u001b[39msession \u001b[38;5;241m=\u001b[39m _make_session()\n\u001b[0;32m    515\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 516\u001b[0m     result \u001b[38;5;241m=\u001b[39m \u001b[43m_thread_context\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msession\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m    517\u001b[0m \u001b[43m        \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    518\u001b[0m \u001b[43m        \u001b[49m\u001b[43mabs_url\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    519\u001b[0m \u001b[43m        \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    520\u001b[0m \u001b[43m        \u001b[49m\u001b[43mdata\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    521\u001b[0m \u001b[43m        \u001b[49m\u001b[43mfiles\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfiles\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    522\u001b[0m \u001b[43m        \u001b[49m\u001b[43mstream\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    523\u001b[0m \u001b[43m        \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest_timeout\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mrequest_timeout\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mTIMEOUT_SECS\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    524\u001b[0m \u001b[43m    \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    525\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m requests\u001b[38;5;241m.\u001b[39mexceptions\u001b[38;5;241m.\u001b[39mTimeout \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m    526\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m error\u001b[38;5;241m.\u001b[39mTimeout(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mRequest timed out: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(e)) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\requests\\sessions.py:529\u001b[0m, in \u001b[0;36mSession.request\u001b[1;34m(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)\u001b[0m\n\u001b[0;32m    524\u001b[0m send_kwargs \u001b[38;5;241m=\u001b[39m {\n\u001b[0;32m    525\u001b[0m     \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mtimeout\u001b[39m\u001b[38;5;124m'\u001b[39m: timeout,\n\u001b[0;32m    526\u001b[0m     \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mallow_redirects\u001b[39m\u001b[38;5;124m'\u001b[39m: allow_redirects,\n\u001b[0;32m    527\u001b[0m }\n\u001b[0;32m    528\u001b[0m send_kwargs\u001b[38;5;241m.\u001b[39mupdate(settings)\n\u001b[1;32m--> 529\u001b[0m resp \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msend(prep, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39msend_kwargs)\n\u001b[0;32m    531\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m resp\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\requests\\sessions.py:645\u001b[0m, in \u001b[0;36mSession.send\u001b[1;34m(self, request, **kwargs)\u001b[0m\n\u001b[0;32m    642\u001b[0m start \u001b[38;5;241m=\u001b[39m preferred_clock()\n\u001b[0;32m    644\u001b[0m \u001b[38;5;66;03m# Send the request\u001b[39;00m\n\u001b[1;32m--> 645\u001b[0m r \u001b[38;5;241m=\u001b[39m adapter\u001b[38;5;241m.\u001b[39msend(request, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[0;32m    647\u001b[0m \u001b[38;5;66;03m# Total elapsed time of the request (approximately)\u001b[39;00m\n\u001b[0;32m    648\u001b[0m elapsed \u001b[38;5;241m=\u001b[39m preferred_clock() \u001b[38;5;241m-\u001b[39m start\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\requests\\adapters.py:440\u001b[0m, in \u001b[0;36mHTTPAdapter.send\u001b[1;34m(self, request, stream, timeout, verify, cert, proxies)\u001b[0m\n\u001b[0;32m    438\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m    439\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m chunked:\n\u001b[1;32m--> 440\u001b[0m         resp \u001b[38;5;241m=\u001b[39m \u001b[43mconn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43murlopen\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m    441\u001b[0m \u001b[43m            \u001b[49m\u001b[43mmethod\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    442\u001b[0m \u001b[43m            \u001b[49m\u001b[43murl\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    443\u001b[0m \u001b[43m            \u001b[49m\u001b[43mbody\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbody\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    444\u001b[0m \u001b[43m            \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    445\u001b[0m \u001b[43m            \u001b[49m\u001b[43mredirect\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m    446\u001b[0m \u001b[43m            \u001b[49m\u001b[43massert_same_host\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m    447\u001b[0m \u001b[43m            \u001b[49m\u001b[43mpreload_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m    448\u001b[0m \u001b[43m            \u001b[49m\u001b[43mdecode_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m    449\u001b[0m \u001b[43m            \u001b[49m\u001b[43mretries\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmax_retries\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    450\u001b[0m \u001b[43m            \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimeout\u001b[49m\n\u001b[0;32m    451\u001b[0m \u001b[43m        \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    453\u001b[0m     \u001b[38;5;66;03m# Send the request.\u001b[39;00m\n\u001b[0;32m    454\u001b[0m     \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m    455\u001b[0m         \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(conn, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mproxy_pool\u001b[39m\u001b[38;5;124m'\u001b[39m):\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\urllib3\\connectionpool.py:670\u001b[0m, in \u001b[0;36mHTTPConnectionPool.urlopen\u001b[1;34m(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)\u001b[0m\n\u001b[0;32m    667\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_prepare_proxy(conn)\n\u001b[0;32m    669\u001b[0m \u001b[38;5;66;03m# Make the request on the httplib connection object.\u001b[39;00m\n\u001b[1;32m--> 670\u001b[0m httplib_response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_make_request\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m    671\u001b[0m \u001b[43m    \u001b[49m\u001b[43mconn\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    672\u001b[0m \u001b[43m    \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    673\u001b[0m \u001b[43m    \u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    674\u001b[0m \u001b[43m    \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimeout_obj\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    675\u001b[0m \u001b[43m    \u001b[49m\u001b[43mbody\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbody\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    676\u001b[0m \u001b[43m    \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    677\u001b[0m \u001b[43m    \u001b[49m\u001b[43mchunked\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mchunked\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m    678\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    680\u001b[0m \u001b[38;5;66;03m# If we're going to release the connection in ``finally:``, then\u001b[39;00m\n\u001b[0;32m    681\u001b[0m \u001b[38;5;66;03m# the response doesn't need to know about the connection. Otherwise\u001b[39;00m\n\u001b[0;32m    682\u001b[0m \u001b[38;5;66;03m# it will also try to release it and we'll have a double-release\u001b[39;00m\n\u001b[0;32m    683\u001b[0m \u001b[38;5;66;03m# mess.\u001b[39;00m\n\u001b[0;32m    684\u001b[0m response_conn \u001b[38;5;241m=\u001b[39m conn \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m release_conn \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\urllib3\\connectionpool.py:426\u001b[0m, in \u001b[0;36mHTTPConnectionPool._make_request\u001b[1;34m(self, conn, method, url, timeout, chunked, **httplib_request_kw)\u001b[0m\n\u001b[0;32m    421\u001b[0m             httplib_response \u001b[38;5;241m=\u001b[39m conn\u001b[38;5;241m.\u001b[39mgetresponse()\n\u001b[0;32m    422\u001b[0m         \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m    423\u001b[0m             \u001b[38;5;66;03m# Remove the TypeError from the exception chain in\u001b[39;00m\n\u001b[0;32m    424\u001b[0m             \u001b[38;5;66;03m# Python 3 (including for exceptions like SystemExit).\u001b[39;00m\n\u001b[0;32m    425\u001b[0m             \u001b[38;5;66;03m# Otherwise it looks like a bug in the code.\u001b[39;00m\n\u001b[1;32m--> 426\u001b[0m             \u001b[43msix\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mraise_from\u001b[49m\u001b[43m(\u001b[49m\u001b[43me\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[0;32m    427\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (SocketTimeout, BaseSSLError, SocketError) \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m    428\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_raise_timeout(err\u001b[38;5;241m=\u001b[39me, url\u001b[38;5;241m=\u001b[39murl, timeout_value\u001b[38;5;241m=\u001b[39mread_timeout)\n",
      "File \u001b[1;32m<string>:3\u001b[0m, in \u001b[0;36mraise_from\u001b[1;34m(value, from_value)\u001b[0m\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\site-packages\\urllib3\\connectionpool.py:421\u001b[0m, in \u001b[0;36mHTTPConnectionPool._make_request\u001b[1;34m(self, conn, method, url, timeout, chunked, **httplib_request_kw)\u001b[0m\n\u001b[0;32m    418\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m:\n\u001b[0;32m    419\u001b[0m     \u001b[38;5;66;03m# Python 3\u001b[39;00m\n\u001b[0;32m    420\u001b[0m     \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 421\u001b[0m         httplib_response \u001b[38;5;241m=\u001b[39m \u001b[43mconn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgetresponse\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    422\u001b[0m     \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m    423\u001b[0m         \u001b[38;5;66;03m# Remove the TypeError from the exception chain in\u001b[39;00m\n\u001b[0;32m    424\u001b[0m         \u001b[38;5;66;03m# Python 3 (including for exceptions like SystemExit).\u001b[39;00m\n\u001b[0;32m    425\u001b[0m         \u001b[38;5;66;03m# Otherwise it looks like a bug in the code.\u001b[39;00m\n\u001b[0;32m    426\u001b[0m         six\u001b[38;5;241m.\u001b[39mraise_from(e, \u001b[38;5;28;01mNone\u001b[39;00m)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\http\\client.py:1377\u001b[0m, in \u001b[0;36mHTTPConnection.getresponse\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m   1375\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m   1376\u001b[0m     \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m-> 1377\u001b[0m         \u001b[43mresponse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbegin\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m   1378\u001b[0m     \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mConnectionError\u001b[39;00m:\n\u001b[0;32m   1379\u001b[0m         \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mclose()\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\http\\client.py:320\u001b[0m, in \u001b[0;36mHTTPResponse.begin\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    318\u001b[0m \u001b[38;5;66;03m# read until we get a non-100 response\u001b[39;00m\n\u001b[0;32m    319\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[1;32m--> 320\u001b[0m     version, status, reason \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_read_status\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    321\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m status \u001b[38;5;241m!=\u001b[39m CONTINUE:\n\u001b[0;32m    322\u001b[0m         \u001b[38;5;28;01mbreak\u001b[39;00m\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\http\\client.py:281\u001b[0m, in \u001b[0;36mHTTPResponse._read_status\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    280\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_read_status\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m--> 281\u001b[0m     line \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreadline\u001b[49m\u001b[43m(\u001b[49m\u001b[43m_MAXLINE\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124miso-8859-1\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m    282\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(line) \u001b[38;5;241m>\u001b[39m _MAXLINE:\n\u001b[0;32m    283\u001b[0m         \u001b[38;5;28;01mraise\u001b[39;00m LineTooLong(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstatus line\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\socket.py:704\u001b[0m, in \u001b[0;36mSocketIO.readinto\u001b[1;34m(self, b)\u001b[0m\n\u001b[0;32m    702\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m    703\u001b[0m     \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 704\u001b[0m         \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_sock\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrecv_into\u001b[49m\u001b[43m(\u001b[49m\u001b[43mb\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m    705\u001b[0m     \u001b[38;5;28;01mexcept\u001b[39;00m timeout:\n\u001b[0;32m    706\u001b[0m         \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_timeout_occurred \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\ssl.py:1241\u001b[0m, in \u001b[0;36mSSLSocket.recv_into\u001b[1;34m(self, buffer, nbytes, flags)\u001b[0m\n\u001b[0;32m   1237\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m flags \u001b[38;5;241m!=\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m   1238\u001b[0m         \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[0;32m   1239\u001b[0m           \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnon-zero flags not allowed in calls to recv_into() on \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m%\u001b[39m\n\u001b[0;32m   1240\u001b[0m           \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__class__\u001b[39m)\n\u001b[1;32m-> 1241\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnbytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbuffer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m   1242\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m   1243\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mrecv_into(buffer, nbytes, flags)\n",
      "File \u001b[1;32mE:\\env\\Anaconda\\lib\\ssl.py:1099\u001b[0m, in \u001b[0;36mSSLSocket.read\u001b[1;34m(self, len, buffer)\u001b[0m\n\u001b[0;32m   1097\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m   1098\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m buffer \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m-> 1099\u001b[0m         \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_sslobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mlen\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbuffer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m   1100\u001b[0m     \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m   1101\u001b[0m         \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_sslobj\u001b[38;5;241m.\u001b[39mread(\u001b[38;5;28mlen\u001b[39m)\n",
      "\u001b[1;31mKeyboardInterrupt\u001b[0m: "
     ]
    }
   ],
   "source": [
    "# 和之前一样，求embedding\n",
    "from openai.embeddings_utils import get_embedding, cosine_similarity\n",
    "from sklearn.metrics.pairwise import cosine_similarity\n",
    "import openai\n",
    "import numpy as np\n",
    "\n",
    "openai.api_key = os.environ.get(\"OPENAI_API_KEY\") # 填入api key\n",
    "\n",
    "sdf[\"embedding\"] = sdf.apply(lambda x:  get_embedding(x.Title + x.Description, engine=\"text-embedding-ada-002\"), axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae444ffc-b5f5-4cc4-a402-0871437544e1",
   "metadata": {},
   "source": [
    "&emsp;&emsp;召回的方法："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fb4f6cdc-77b2-4ddd-9be9-d42cfeea0fcd",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "class Recall:\n",
    "    \n",
    "    def __init__(self, df: pd.DataFrame):\n",
    "        self.data = df\n",
    "    \n",
    "    def user_prefer_recall(self, user, n):\n",
    "        up = self.get_user_prefers(user)\n",
    "        idx = random.randrange(0, len(up.prefers))\n",
    "        return self.pick_by_idx(idx, n)\n",
    "    \n",
    "    def hot_recall(self, n):\n",
    "        # 随机进行示例\n",
    "        df = self.data.sample(n)\n",
    "        return df\n",
    "    \n",
    "    def user_action_recall(self, user, n):\n",
    "        actions = self.get_user_actions(user)\n",
    "        interest = self.get_most_interested_item(actions)\n",
    "        recoms = self.recommend_by_interest(interest, n)\n",
    "        return recoms\n",
    "    \n",
    "    def get_most_interested_item(self, user_action):\n",
    "        \"\"\"\n",
    "        可以选近一段时间内用户交互时间、次数、评论（相关属性）过的Item\n",
    "        \"\"\"\n",
    "        # 就是sdf的第2行，idx为1的那条作为最喜欢（假设）\n",
    "        # 是一条游泳相关的Item\n",
    "        idx = user_action.item.item_props[\"id\"]\n",
    "        im = self.data.iloc[idx]\n",
    "        return im\n",
    "    \n",
    "    def recommend_by_interest(self, interest, n):\n",
    "        cate_id = interest[\"Class Index\"]\n",
    "        q_emb = interest[\"embedding\"]\n",
    "        # 确定类别\n",
    "        base = self.data[self.data[\"Class Index\"] == cate_id]\n",
    "        # 此处可以复用QA那一段代码，用给定embedding计算base中embedding的相似度\n",
    "        base_arr = np.array(\n",
    "            [v.embedding for v in base.itertuples()]\n",
    "        )\n",
    "        q_arr = np.expand_dims(q_emb, 0)\n",
    "        sims = cosine_similarity(base_arr, q_arr)\n",
    "        # 排除掉自己\n",
    "        idxes = sims.argsort(0).squeeze()[-(n+1):-1]\n",
    "        return base.iloc[reversed(idxes.tolist())]\n",
    "    \n",
    "    def pick_by_idx(self, category, n):\n",
    "        df = self.data[self.data[\"Class Index\"] == category]\n",
    "        return df.sample(n)\n",
    "    \n",
    "    def get_user_actions(self, user):\n",
    "        dct = {\"u1\": ua1}\n",
    "        return dct[user.user_name]\n",
    "    \n",
    "    def get_user_prefers(self, user):\n",
    "        dct = {\"u1\": up1}\n",
    "        return dct[user.user_name]\n",
    "    \n",
    "    def run(self, user):\n",
    "        ur = self.user_action_recall(user, 5)\n",
    "        if len(ur) == 0:\n",
    "            ur = self.user_prefer_recall(user, 5)\n",
    "        hr = self.hot_recall(3)\n",
    "        return pd.concat([ur, hr], axis=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16b2130b-7db0-4536-82f1-7d0c42ce7701",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 初始化文章库\n",
    "r = Recall(sdf)\n",
    "# 根据用户u1进行召回\n",
    "rd = r.run(u1)\n",
    "rd"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0f8cabe3-75e7-4708-9425-dd5cfb0453a3",
   "metadata": {},
   "source": [
    "补充说明，上面的只是一个大致的流程，实际中有很多细节或优化点需要注意，比如：\n",
    "\n",
    "1. 建数据库表（上面的`get_`其实都是查表）\n",
    "2. 将Item、User和Action也进行Embedding，全部使用Embedding后再做召回\n",
    "3. 对『感兴趣`get_most_interested_item`』更多的优化，考虑更多行为和反馈，召回更多不同类型条目\n",
    "4. 性能和自动更新数据的考虑\n",
    "5. 线上评测，A/B等"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c41fe025-fc9f-4aaa-a907-4495056f6aee",
   "metadata": {},
   "source": [
    "可以发现，我们虽然只做了召回一步，但其中涉及到的内容已经远远不止之前QA那一点了，QA用到的东西可能只是其中一小部分。不过事无绝对，即便是QA任务也可能根据实际情况不同需要做很多优化，比如召回+排序。但总体来说，类似推荐这样比较综合的系统相对来说会更加复杂一些。</br>\n",
    "后面就是排序了，这一步需要区分不同的应用场景，可以做或不做，做的话（就是对刚刚得到的列表进行排序）也可以简单或复杂。比如简单地按发布时间，复杂的综合考虑多样性、时效性、用户反馈、热门程度等多种属性。具体操作时，可以直接按相关属性排序，也可以用模型排序。这里就不再继续深入了。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f3aca980-f198-4b1a-911e-172ac3611c46",
   "metadata": {},
   "source": [
    "## 相关文献\n",
    "\n",
    "- 【1】[浅析文本分类 —— 情感分析与自然语言处理 | Yam](https://yam.gift/2021/10/27/NLP/2021-10-27-Senta/)\n",
    "- 【2】[句子表征综述 | Yam](https://yam.gift/2022/03/27/NLP/2022-03-27-Sentence-Representation-Summarization/)\n",
    "- 【3】[NLP 表征的历史与未来 | Yam](https://yam.gift/2020/12/12/NLP/2020-12-12-NLP-Representation-History-Future/)\n",
    "\n",
    "\n",
    "memo：\n",
    "\n",
    "- [openai-cookbook/Semantic_text_search_using_embeddings.ipynb at main · openai/openai-cookbook](https://github.com/openai/openai-cookbook/blob/main/examples/Semantic_text_search_using_embeddings.ipynb)\n",
    "- [openai-cookbook/getting-started-with-redis-and-openai.ipynb at main · openai/openai-cookbook](https://github.com/openai/openai-cookbook/blob/main/examples/vector_databases/redis/getting-started-with-redis-and-openai.ipynb)\n",
    "- [openai-cookbook/Visualizing_embeddings_in_3D.ipynb at main · openai/openai-cookbook](https://github.com/openai/openai-cookbook/blob/main/examples/Visualizing_embeddings_in_3D.ipynb)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14a2148c-b0a5-437f-82ba-9ee7d5a5d97a",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
